package my.StudentAdministratie;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.util.AbstractList;
import java.util.ArrayList;
import java.util.List;
import java.util.Stack;

import org.xml.sax.Attributes;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.XMLReader;
import org.xml.sax.helpers.AttributesImpl;
import org.xml.sax.helpers.DefaultHandler;
import org.xml.sax.helpers.XMLReaderFactory;

public class XMLWrapper {	
	public class XMLException extends RuntimeException {
		private static final long serialVersionUID = -3847237290382658745L;
		
		public XMLException(String message) {
			super(message);
		}
		
		public XMLException(String message, Throwable cause) {
			super(message, cause);
		}
	}
	
	public class StudentList extends AbstractList<Student> {
		private class StudentNode {
			@SuppressWarnings("unused")
			public StudentNode next, prev;
			public Student student;
			
			public StudentNode(Student s) {
				student = s;
			}
		}
		
		private StudentNode header, current;
		private int currentIndex = -1;
		
		private int maxsize = 100;
		
		/**
		 * Offset from which we have loaded data
		 */
		private int offset = 0;
		
		/**
		 * Amount of loaded data
		 */
		private int loadedsize = 0;
		
		/**
		 * Creates a new StudentList with default max size (100)
		 */
		private StudentList() {
			header = current = new StudentNode(null);
			header.next = header.prev = header;
		}
		
		/**
		 * Creates a new StudentList with the specified max size
		 * @param maxsize
		 */
		private StudentList(int maxsize) {
			this();
			
			this.maxsize = maxsize;
		}

		@Override
		public Student get(int index) {
			//Check bounds
			if(index < 0 || index >= size())
				throw new IndexOutOfBoundsException("index < 0 || index >= size()");
			
//			System.out.println("get(" + index + ") - " + offset + ", " + (offset + loadedsize));
			
			if(index < offset) {
//				System.out.println(index + " < " + offset);
				
				//We are requesting a student, before currently loaded region
				offset = index - (maxsize / 4);
				if(offset < 0)
					offset = 0;
				
				//Load students
				ArrayList<Student> students = loadStudentRange(offset, maxsize);
				
				//Update the list
				setListData(students);
			} else if(index >= offset + loadedsize) {
//				System.out.println(index + " >= " + offset + "+" + loadedsize);
				
				//We are requesting a student, after currently loaded region
				offset = index - (maxsize - (maxsize / 4));
				if(offset < 0)
					offset = 0;
				
				//Load students
				ArrayList<Student> students = loadStudentRange(offset, maxsize);
				
				//Update the list
				setListData(students);
			}
			
			//Find student, which we always have when we reach here
			int targetIndex = (index - offset) - currentIndex;
			if(targetIndex < 0) {
				//Reset current
				current = header;
				currentIndex = -1;
				
				//Recalculate target
				targetIndex = (index - offset) - currentIndex;
			}
			while(targetIndex-- > 0) {
				current = current.next;
				currentIndex++;
			}
			
			return current.student;
		}

		@Override
		public int size() {
			
			return XMLWrapper.this.size();
		}
		
		private void setListData(ArrayList<Student> students) {
			//Updated loadedsize
			loadedsize = students.size();
			
			//Fill the list
			StudentNode sn = header;
			for(Student s : students) {
				sn.next = new StudentNode(s);
				sn.next.prev = sn;
				sn = sn.next;
			}
			
			//Update header links
			sn.next = header;
			header.prev = sn;
			
			//Reset current
			current = header;
			currentIndex = -1;
		}
	}
	
	private class StudentRangeHandler extends DefaultHandler {
		private ArrayList<Student> students;
		private Stack<String> elementDepth = new Stack<String>();
		
		private int offset, size;
		private int pos = -1;
		
		public StudentRangeHandler(int offset, int size) {
			this.students = new ArrayList<Student>(size);
			this.offset = offset;
			this.size = size;
		}
		
		public ArrayList<Student> getStudents() {
			return students;
		}

		@Override
		public void endElement(String namespaceURI, String localName, String qName) throws SAXException {
			//TODO: break out here when we processed enough
			
			//Pop the current element
			elementDepth.pop();
		}

		@Override
		public void startElement(String namespaceURI, String localName, String qName, Attributes atts) throws SAXException {
			//Check if we can parse a student
			if(elementDepth.size() == 2 && elementDepth.peek().equalsIgnoreCase("studenten")) {
				this.pos++;
				
				//Check bounds
				if(pos >= offset && pos < offset + size) {
					students.add(new Student(
							atts.getValue("id"), 
							atts.getValue("naam"), 
							atts.getValue("adres"), 
							atts.getValue("postcode"), 
							atts.getValue("woonplaats"), 
							atts.getValue("geslacht"), 
							atts.getValue("telefoonnr"),
							atts.getValue("inschrijvingsdatum")
					));
				}
			}
			
			//Add current name to the elementDepth stack
			elementDepth.add(localName);
		}
	}
	
	private class StudentCountHandler extends DefaultHandler {
		private Stack<String> elementDepth = new Stack<String>();
		private int count = 0;
		
		public int getStudentCount() {
			return count;
		}

		@Override
		public void endElement(String namespaceURI, String localName, String qName) throws SAXException {
			//TODO: break out here when we processed enough
			
			//Pop the current element
			elementDepth.pop();
		}

		@Override
		public void startElement(String namespaceURI, String localName, String qName, Attributes atts) throws SAXException {
			//Check if we can parse a student
			if(elementDepth.size() == 2 && elementDepth.peek().equalsIgnoreCase("studenten")) {
				this.count++;
			}
			
			//Add current name to the elementDepth stack
			elementDepth.add(localName);
		}
	}
	
	private class StudentWriter extends XMLWriter {
		private Stack<String> elementDepth = new Stack<String>();
		private Student student;
		private boolean modify;
		private boolean modified = false;
		
		public StudentWriter(XMLReader parent, Writer writer, Student student, boolean modify) {
			super(parent, writer);
			
			this.student = student;
			this.modify = modify;
		}

		@Override
		public void startElement(String uri, String localName, String qName, Attributes atts) throws SAXException {
			//Check if we modify the student
			if(modify && elementDepth.size() == 2 && elementDepth.peek().equalsIgnoreCase("studenten") && atts.getValue("id").equalsIgnoreCase(student.studentnummer)) {
				atts = createAttributes();
				modified = true;
			}
			
			//Check if we add the student and there is a student with the same id
			if(!modify && elementDepth.size() == 2 && elementDepth.peek().equalsIgnoreCase("studenten") && atts.getValue("id").equalsIgnoreCase(student.studentnummer)) {
				throw new XMLException("Duplicate student with id: " + student.studentnummer);
			}
			
			//Add current name to the elementDepth stack
			elementDepth.add(localName);
			
			//Write the start element
			super.startElement(uri, localName, qName, atts);
		}

		@Override
		public void endElement(String uri, String localName, String qName) throws SAXException {
			//Check if we add the student
			if(!modify && elementDepth.size() == 2 && elementDepth.peek().equalsIgnoreCase("studenten") && localName.equalsIgnoreCase("studenten")) {
				//Create attributes
				Attributes atts = createAttributes();
				
				//Create student
				super.startElement("", "student", "student", atts);
				super.endElement("", "student", "student");
			}
			
			//Check if we modify the student
			if(modify && !modified && elementDepth.size() == 2 && elementDepth.peek().equalsIgnoreCase("studenten") && localName.equalsIgnoreCase("studenten")) {
				throw new XMLException("Couldn't edit unknown student with id: " + student.studentnummer);
			}
			
			//Pop the current element
			elementDepth.pop();
			
			//Write the endElement
			super.endElement(uri, localName, qName);
		}
		
		private Attributes createAttributes() {
			AttributesImpl att = new AttributesImpl();
			att.addAttribute("", "adres", "adres", "CDATA", student.adres);
			att.addAttribute("", "geslacht", "geslacht", "CDATA", student.geslacht);
			att.addAttribute("", "id", "id", "CDATA", student.studentnummer);
			att.addAttribute("", "inschrijvingsdatum", "inschrijvingsdatum", "CDATA", student.inschrijvingsdatum);
			att.addAttribute("", "naam", "naam", "CDATA", student.naam);
			att.addAttribute("", "postcode", "postcode", "CDATA", student.postcode);
			att.addAttribute("", "telefoonnr", "telefoonnr", "CDATA", student.telefoonnummer);
			att.addAttribute("", "woonplaats", "woonplaats", "CDATA", student.woonplaats);
			
			return att;
		}
	}
	
	/**
	 * The callbacks to run, when we edit or add a student
	 */
	private ArrayList<Runnable> updateCallbacks = new ArrayList<Runnable>();
	
	/**
	 * The file that hols our xml
	 */
	private File file;
	
	/**
	 * Total amount of data available
	 */
	private int size = -1;
	
	/**
	 * Our xml reader (using sax)
	 */
	private XMLReader xmlReader;
	
	/**
	 * Our list of students, will dynamically load the students
	 */
	private StudentList students;
	
	public XMLWrapper(String path) {
		file = new File(path);
		try {
			if(!file.exists())
				file.createNewFile();
		} catch(IOException e) {
			throw new XMLException("Error creating new xml file", e);
		}
		
		try {
			xmlReader = XMLReaderFactory.createXMLReader();
		} catch(SAXException se) {
			throw new XMLException("Error while creating xml readerer", se);
		}
		
		students = new StudentList();
	}
	
	public void addUpdateCallback(Runnable r) {
		updateCallbacks.add(r);
	}
	
	public void removeUpdateCallback(Runnable r) {
		updateCallbacks.remove(r);
	}
	
	public List<Student> getStudentList() {
		return students;
	}
	
	/**
	 * This method returns the size of the students.
	 * Will load it if not loaded before
	 */
	private int size() {
		if(size == -1) {
			StudentCountHandler handler = new StudentCountHandler();
			try {
				synchronized(xmlReader) {
					xmlReader.setContentHandler(handler);
					xmlReader.parse(new InputSource(new FileInputStream(file)));
				}
			} catch(SAXException e) {
				throw new XMLException("Error loading student count", e);
			} catch(IOException e) {
				throw new XMLException("Error reading xml file", e);
			}
			
			size = handler.getStudentCount();
		}
		
		return size;
	}
	
	private ArrayList<Student> loadStudentRange(int off, int size) {
		StudentRangeHandler handler = new StudentRangeHandler(off, size);
		try {
			synchronized(xmlReader) {
				xmlReader.setContentHandler(handler);
				xmlReader.parse(new InputSource(new FileInputStream(file)));
			}
		} catch(SAXException e) {
			throw new XMLException("Error loading student range", e);
		} catch(IOException e) {
			throw new XMLException("Error reading xml file", e);
		}
		
		return handler.getStudents();
	}
	
	public void writeStudent(Student s, boolean modify) {
		try {
			//Create tmp file
			File tmp = File.createTempFile("sa_", ".xml");
			
			//Write the student to the tmp file
			XMLWriter writer = new StudentWriter(xmlReader, new OutputStreamWriter(new FileOutputStream(tmp)), s, modify);
			writer.parse(new InputSource(new FileInputStream(file)));
			
			//Copy tmp file into default file
			InputStream fis = new FileInputStream(tmp);
			OutputStream fos = new FileOutputStream(file);
			
			byte[] buffer = new byte[16384];
			int read;
			while( (read = fis.read(buffer)) != -1 ) {
				fos.write(buffer, 0, read);
			}
			fis.close();
			fos.flush();
			fos.close();
			
			//Increase size if we added something
			if(!modify)
				this.size++;
			
			//Call callbacks
			for(Runnable r : updateCallbacks)
				r.run();
			
		} catch(SAXException e) {
			throw new XMLException("Error writing student", e);
		} catch(IOException e) {
			throw new XMLException("Error writing xml file", e);
		}
	}
}
